package network

import (
	"JoDocker/container"
	"encoding/json"
	"fmt"
	"github.com/Sirupsen/logrus"
	"github.com/vishvananda/netlink" // go语言的操作网络接口，路由表等配置的库，使用它的调用相当于我们通过ip命令去管理网络接口
	"github.com/vishvananda/netns"   // go语言版进出Net Namespace的库，通过这个库，可以让netlink库中配置网络接口的代码在Net Namespace中执行
	"net"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"runtime"
	"strings"
	"text/tabwriter"
)

var (
	defaultNetWorkPath = "/var/run/jodocker/network/network/"
	driverMap          = map[string]NetworkDriver{} // 存储驱动
	networkMap         = map[string]*Network{}      // 存储当前已经创建的网络
)

// Endpoint 网络端点是用于链接容器与网络的，保证容器内部与网络的通信，像veth一端挂载到容器内部， 另一端挂载到bridge上，保证容器和网络的通信
type Endpoint struct {
	ID             string           `json:"id"`
	Device         netlink.Veth     `json:"dev"` // 虚拟网络设备
	IpAddress      net.IP           `json:"ip"`  // IP地址
	MacAddress     net.HardwareAddr `json:"mac"` // MAC地址
	NetworkPtr     *Network
	PortMappingSlc []string // 端口映射
}

// Network 是容器的一个集合，在这个网络上的容器可以通过这个网络相互通信。
// 挂在到同一个bridge设备上的容器可以直接通过bridge设备实现网络互联。
type Network struct {
	Name       string     // 网络名称
	IpRangePtr *net.IPNet // 网段（地址段）
	Driver     string     // 网络驱动名称
}

// NetworkDriver 网络驱动
// 不同的驱动对网络的创建、链接、销毁的策略不同。通过在创建网络时指定不同的网络驱动来定义使用哪个驱动做网络的配置。
// 本例使用bridge驱动
type NetworkDriver interface {
	Name() string                                             // 驱动名
	Create(subnet string, name string) (*Network, error)      // 创建网络
	Delete(network Network) error                             // 删除网络
	Connect(networkPtr *Network, endpointPtr *Endpoint) error // 链接容器网络端点到网络
	Disconnect(network Network, endpointPtr *Endpoint) error  // 从网络上移除容器网络端点
}

// 把network（网络名，地址段，网络驱动名）写入到文件中
func (this *Network) dump(dumpPath string) error {
	if _, err := os.Stat(dumpPath); err != nil {
		if os.IsNotExist(err) {
			os.MkdirAll(dumpPath, 0644)
		} else {
			return err
		}
	}
	nwPath := path.Join(dumpPath, this.Name)
	nwFile, err := os.OpenFile(nwPath, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644)
	if err != nil {
		logrus.Errorf("open file %s error: %v", nwPath, err)
		return err
	}
	defer nwFile.Close()

	nwJson, err := json.Marshal(this)
	if err != nil {
		logrus.Errorf("json marshal error : %v", err)
		return err
	}

	_, err = nwFile.Write(nwJson)
	if err != nil {
		logrus.Errorf("write file %s error %v", nwPath, err)
		return err
	}
	return nil
}

// 删除network文件
func (this *Network) remove(dumpPath string) error {
	// 网络对应的配置文件 即配置目录下的网络名文件
	// 检查文件状态, 如果文件已经不存在就直接返回
	nwPath := path.Join(dumpPath, this.Name)
	if _, err := os.Stat(nwPath); err != nil {
		if os.IsNotExist(err) {
			return nil
		} else {
			return err
		}
	} else {
		// 删除网络配置文件
		return os.Remove(nwPath)
	}
}

// 从文件中加载network数据
func (this *Network) load(dumpPath string) error {
	nwConfigFile, err := os.Open(dumpPath)
	defer nwConfigFile.Close()
	if err != nil {
		return err
	}
	nwJson := make([]byte, 2000)
	n, err := nwConfigFile.Read(nwJson)
	if err != nil {
		return err
	}

	err = json.Unmarshal(nwJson[:n], this)
	if err != nil {
		logrus.Errorf("unmarshal Error load nw info %v", err)
		return err
	}
	return nil
}

// Init 网络初始化 初始化网桥驱动和已经配置的网络信息
func Init() error {
	// 网络驱动
	var bridgeDriver = BridgeNetworkDriver{}
	driverMap[bridgeDriver.Name()] = &bridgeDriver

	if _, err := os.Stat(defaultNetWorkPath); err != nil {
		if os.IsNotExist(err) {
			os.MkdirAll(defaultNetWorkPath, 0644)
		} else {
			return err
		}
	}

	// 遍历网络配置目录中的所有文件
	filepath.Walk(defaultNetWorkPath, func(nwPath string, info os.FileInfo, err error) error {
		// 是目录则跳过
		if strings.HasSuffix(nwPath, "/") {
			return nil
		}
		// 加载文件名作为网络名
		_, nwName := path.Split(nwPath)
		nwPtr := &Network{
			Name: nwName,
		}
		// 加载对应的网络配置信息
		if err := nwPtr.load(nwPath); err != nil {
			logrus.Errorf("error load network: %s", err)
		}
		// 将网络的配置信息加入到networkMap中
		networkMap[nwName] = nwPtr
		return nil
	})

	return nil
}

// CreateNetwork 创建网络
// docker network create --subnet 192.168.0.0/24 --driver bridge testbridgenet
// 网段：192.168.0.0/24
// 网络驱动：bridge
// 网络名称：testbridgenet
func CreateNetwork(driver, subnet, name string) error {
	// 将网段的字符串转换成net.IPNet对象
	_, cidr, _ := net.ParseCIDR(subnet)
	// 通过IPAM分配网关IP，获取到网段中第一个IP作为网关的IP
	gatewayIp, err := ipAllocator.Allocate(cidr)
	if err != nil {
		return err
	}
	cidr.IP = gatewayIp

	logrus.Infof("CreateNetwork => subnet:%s, name:%s, gatewayIp:%s", subnet, name, gatewayIp.String())

	// 调用指定的网络驱动创建网络
	nwPtr, err := driverMap[driver].Create(cidr.String(), name)
	if err != nil {
		return err
	}
	// 保存网络信息 将网络的信息保存在文件中，以便查询和在网络上链接网络端点
	return nwPtr.dump(defaultNetWorkPath)
}

// ListNetWork 输出网络列表
func ListNetWork() {
	writer := tabwriter.NewWriter(os.Stdout, 12, 1, 3, ' ', 0)
	fmt.Fprint(writer, "NAME\tIpRange\tDriver\n")
	// 遍历网络信息
	for _, nwPtr := range networkMap {
		fmt.Fprintf(writer, "%s\t%s\t%s\n", nwPtr.Name, nwPtr.IpRangePtr.String(), nwPtr.Driver)
	}
	if err := writer.Flush(); err != nil {
		logrus.Errorf("Flush error %v", err)
		return
	}
}

// DeleteNetwork 删除网络
func DeleteNetwork(networkName string) error {
	nwPtr, ok := networkMap[networkName]
	if !ok {
		return fmt.Errorf("No such Network: %s", networkName)
	}

	// 释放网络网关ip
	if err := ipAllocator.Release(nwPtr.IpRangePtr, &nwPtr.IpRangePtr.IP); err != nil {
		return fmt.Errorf("Error Remove Network gatewy ip: %v", err)
	}

	// 删除网络创建的设备和配置
	if err := driverMap[nwPtr.Driver].Delete(*nwPtr); err != nil {
		return fmt.Errorf("Error Remove Network DriverError: %s", err)
	}
	// 从网络的配置目录中删除该网络对应的配置文件
	return nwPtr.remove(defaultNetWorkPath)
}

// 进入容器Net Namespace
// 将容器的网络端点加入到容器的网络空间中
// 并锁定当前程序所执行的线程, 使当前线程进入到容器的网络空间
// 返回值是一个函数指针, 执行这个返回函数才会退出容器的网络空间, 回归到宿主机的网络空间
func enterContainerNetNs(enLinkPtr *netlink.Link, containerInfoPtr *container.ContainerInfo) func() {
	// 找到容器的net namespace
	// /proc/pid/ns/net 打开这个文件的文件描述符就可以来操作net namespace
	// 而ContainerInfo的PID就是容器在宿主机映射的进程ID
	// 它对应的/proc/pid/ns/net 就是容器内部的net namespace
	file, err := os.OpenFile(fmt.Sprintf("/proc/%s/ns/net", containerInfoPtr.Pid), os.O_RDONLY, 0)
	if err != nil {
		logrus.Errorf("error get container net namespace, %v", err)
	}

	// 取到文件的文件描述符
	nsFd := file.Fd()

	// 锁定当前程序所执行的线程, 如果不锁定操作系统线程的话,GO语言的gorouting可能会被调度到别的线程上去, 就不能保证一直在所需要的网络空间中了.
	// 所以调用runtime.LocakOSThread 锁定当前程序执行的线程
	runtime.LockOSThread()

	// 修改veth peer 另外一端移到容器的namespace中
	// 修改网络端点veth的另外一端,将其移动到容器的net namespace中
	if err = netlink.LinkSetNsFd(*enLinkPtr, int(nsFd)); err != nil {
		logrus.Errorf("error set link netns , %v", err)
	}

	// 获取当前的网络namespace
	// 以便后面从容器的net namespace中退出, 回到原本网络的net namespace中
	origin, err := netns.Get()
	if err != nil {
		logrus.Errorf("error get current netns, %v", err)
	}

	// 调用netns.set方法, 设置当前进程加入到容器的net namespace， 并在函数执行完成之后再恢复到之前的namespace
	if err = netns.Set(netns.NsHandle(nsFd)); err != nil {
		logrus.Errorf("error set netns, %v", err)
	}

	// 返回之前的net namespace
	// 在容器的网络空间中, 执行完容器配置之后调用此函数就可以将程序恢复到原来的net namespace
	return func() {
		// 恢复到上面取到的net namespace
		netns.Set(origin)
		// 关闭namespace文件
		origin.Close()
		// 取消当前程序的线程锁定
		runtime.UnlockOSThread()
		// 关闭namespace文件
		file.Close()
	}
}

// 进入到容器的网络namespace配置网络端点的ip地址和路由
func configEndpointIpAddressAndRoute(epPtr *Endpoint, containerInfoPtr *container.ContainerInfo) error {
	// 通过网络端点中veth的另一端
	peerLink, err := netlink.LinkByName(epPtr.Device.PeerName)
	if err != nil {
		return fmt.Errorf("fail config endpoint: %v", err)
	}

	// 将容器的网络端点加入到容器的网络空间中
	// 并使这个函数下面的操作都在这个网络空间中进行
	// 执行完函数后, 恢复为默认的网络空间
	defer enterContainerNetNs(&peerLink, containerInfoPtr)()

	gatewayIp := epPtr.NetworkPtr.IpRangePtr.IP

	logrus.Infof("configEndpoint -> gatewayIp:%v", gatewayIp)

	// 获取到容器的ip地址和网段, 用于配置容器内部接口地址
	// 比如容器ip:192.168.1.2 而网络的网段是192.168.1.0/24
	// 那么这里产出的ip字符串就是192.168.1.2/24, 用于容器内veth端点配置
	interfaceIp := epPtr.NetworkPtr.IpRangePtr
	// 容器ip
	interfaceIp.IP = epPtr.IpAddress

	//ip netns exec ns1 ifconfig veth1 172.18.0.2/24 up

	// 调用setInterfaceIP设置容器内veth端点的IP
	if err = setInterfaceIP(epPtr.Device.PeerName, interfaceIp.String()); err != nil {
		return fmt.Errorf("%v,%s", epPtr.NetworkPtr, err)
	}

	// 启动容器内的veth端点
	if err = setInterfaceUP(epPtr.Device.PeerName); err != nil {
		return err
	}

	// net namespace中默认本地地址127.0.0.1的lo网卡是关闭状态的
	// 启动它可以保证容器访问自己的请求
	if err = setInterfaceUP("lo"); err != nil {
		return err
	}

	// ip netns exec ns1 route add default dev veth1
	// 将网段的字符串转换成net.IPNet对象
	// 设置容器内的外部请求都通过容器内的veth端点访问 (在net namespace中所有的流量都经过veth设备)
	// 0.0.0.0/0的网段 表示所有的IP地址段
	_, cidr, _ := net.ParseCIDR("0.0.0.0/0")

	// 构建要添加的路由数据, 包括网络设备、网关ip及目的网段
	// 相当于 route add -net 0.0.0.0/0 gw {bridge网桥地址IP} dev {容器内的veth端点设备}
	defaultRoute := &netlink.Route{
		LinkIndex: peerLink.Attrs().Index,
		//Gw:        epPtr.NetworkPtr.IpRangePtr.IP,
		Gw:  gatewayIp,
		Dst: cidr,
	}

	logrus.Infof("netns的路由配置: %v", epPtr)

	// 调用netlink.RouteAdd方法,添加路由到容器的网络空间
	// 相当于route add命令
	if err = netlink.RouteAdd(defaultRoute); err != nil {
		return err
	}
	return nil
}

// 配置端口映射
// iptables中的DNAT策略也是网络地址转换，不过他是要更换目标地址，经常用于将内部地址的端口映射到外部去（容器端口映射到主机端口上），
// 在namespace中需要提供服务给宿主机之外的应用去请求就需要用到DNAT策略
// 把宿主机上的端口TCP、UDP请求转发到namespace地址（172.18.0.2:8080）
func configPortMapping(epPtr *Endpoint, containerInfoPtr *container.ContainerInfo) error {
	// 遍历容器端口映射列表
	for _, pm := range epPtr.PortMappingSlc {
		// 分割成宿主机的端口和容器的端口
		portMapping := strings.Split(pm, ":")
		if len(portMapping) != 2 {
			logrus.Errorf("port mapping format error ,%v", pm)
			continue
		}

		// 在iptables的PREROUTING中添加DNAT规则
		// 将宿主机上的端口请求转发到容器的地址和端口上
		iptablesCmd := fmt.Sprintf("-t nat -A PREROUTING ! -i %s -p tcp -m tcp --dport %s -j DNAT --to-destination %s:%s", epPtr.NetworkPtr.Name, portMapping[0], epPtr.IpAddress.String(), portMapping[1])
		cmd := exec.Command("iptables", strings.Split(iptablesCmd, " ")...)
		logrus.Infof("端口映射 cmd:%s", cmd.String())
		// 执行iptables命令, 添加端口映射转发规则
		output, err := cmd.Output()
		if err != nil {
			logrus.Errorf("iptables output, %v", output)
			continue
		}
	}
	return nil
}

// Connect 链接网络
// docker run -ti -p 80:80 --net testbridgeent busybox
// 通过testbridgenet这个网络与网络中的其他容器进行通信
// 连接容器到之前创建的网络, 使容器加入网络及端口映射
func Connect(networkName string, containerInfoPtr *container.ContainerInfo) error {
	// 从networkMap字典中获取容器链接的网络信息，找不到返回错误
	networkPtr, ok := networkMap[networkName]
	if !ok {
		// 链接的网络不存在
		return fmt.Errorf("No Such Network: %s", networkName)
	}

	logrus.Infof("network文件中加载的%s, network数据:%v", networkName, networkPtr)

	// 获取容器ip -- 分配未使用的ip地址
	ip, err := ipAllocator.Allocate(networkPtr.IpRangePtr)
	if err != nil {
		return err
	}

	logrus.Infof("获取到容器的IP:%s, network数据:%v", ip.String(), networkPtr)

	// 创建网络端点, 设置网络端点的ip、网络和端口映射信息 供下面的配置调用
	epPtr := &Endpoint{
		ID:             fmt.Sprintf("%s-%s", containerInfoPtr.Id, networkName),
		IpAddress:      ip,
		NetworkPtr:     networkPtr,
		PortMappingSlc: containerInfoPtr.PortMapping,
	}
	// 调用网络驱动的connect去链接，挂载和配置网络端点
	if err = driverMap[networkPtr.Driver].Connect(networkPtr, epPtr); err != nil {
		return err
	}

	logrus.Infof("连接网桥后 network数据:%v", networkPtr)

	// 进入到容器的网络namespace配置网络veth设备的ip地址和路由
	if err = configEndpointIpAddressAndRoute(epPtr, containerInfoPtr); err != nil {
		return err
	}
	// 配置容器到宿主机的端口映射 -- DNAT
	// docker run -p 80:80
	return configPortMapping(epPtr, containerInfoPtr)
}

func Disconnect(networkName string, containerInfoPtr *container.ContainerInfo) error {
	return nil
}
